BGP ASNの番号からIPアドレス調査してAWS WAFでフィルターしてみた
こんにちは、コカコーラ大好きカジです。
お客様の要望により、BGP ASNの番号を基にIPアドレスを調査し、AWS WAFでフィルタリングを行う方法を試しました。ここでは、そのアーキテクチャと具体的な実装方法について詳しく解説します。
アーキテクチャの概要
Lambdaが定期的にBGPview APIからASNに関連するIP情報を取得し、AWS WAFのIPセットにその情報を反映する仕組みです。LambdaはCloudWatchによってスケジュールされ、結果はCloudWatch Logsに記録されます。このAWS WAFのIP setsは、CloudFrontやALBでのトラフィックフィルタリングに活用されます。
-
外部データソース(ASN情報取得サービス)
外部サービス(BGPview API)を使用して、特定のASNに関連するIPプレフィックスを取得します。
-
AWS Lambda
Lambda関数が1日1回実行され、外部サービスからASNに基づくIPアドレス範囲を取得し、AWS WAFのIPセットを更新します。
- ライブラリ
requests
を使用して外部APIからデータを取得。 boto3
を使用してAWS WAFのIPセットを取得し、更新します。
- ライブラリ
-
AWS WAF (Web Application Firewall)
WAFにはCloudFrontやApplication Load Balancerに対するフィルタリングを行うIPセットが設定されており、Lambda関数がこれを更新します。
- IPセットは定期的に更新され、特定のASNに基づくIPアドレスブロックが追加されます。
-
Amazon CloudWatch:(今回対象外)
CloudWatchはLambdaのスケジュール実行を管理し、1日1回Lambda関数をトリガーします。また、Lambda関数の実行ログはCloudWatch Logsに記録されます。
構成図
前提条件
- 実際の運用を行なっていないため、考慮漏れなどが考えられます。導入にはご自分の環境で検証の上、ご利用お願いいたします。
- CloudFrontに接続したAWS WAFとブロックリストを入れるIP Setを構築済みとします。
- Python素人なので、IP情報が取れなかった場合の状況は考慮していません。
AWS CLIでIPsetのIDとARNを取得する
作成済みの、AWS WAFのIP SetsのIDやARNをAWS CLIで確認します。
aws wafv2 list-ip-sets --scope CLOUDFRONT --region <region>
{
"IPSets": [
{
"Name": "test-kaji-web-acl-Custom-ipaddress-blocklist",
"Id": "6a0d328b-8e51-463b-af98-xxxxxxxxxxxxx",
"Description": "",
"LockToken": "236ab2bd-f8d8-4aa9-b756-xxxxxxxxxxxxx",
"ARN": "arn:aws:wafv2:<region>:<account-id>:global/ipset/test-kaji-web-acl-Custom-ipaddress-blocklist/6a0d328b-8e51-463b-af98-xxxxxxxxxxxxx"
}
]
}
Lambdaのコード
import json
import boto3
import requests
# WAF IPセットの情報を設定
WAF_IP_SET_ID = 'your-ip-set-id' # 作成したWAF IPセットのIDをここに記入
WAF_SCOPE = 'CLOUDFRONT' # 'REGIONAL' または 'CLOUDFRONT' に変更
# BGPview APIで特定のASNからIPプレフィックスを取得
ASN = 'xxxxxx' # フィルタリングしたいASNをここに記入
def lambda_handler(event, context):
waf_client = boto3.client('wafv2')
try:
# BGPview APIからデータ取得
response = requests.get(f'https://api.bgpview.io/asn/{ASN}/prefixes')
except requests.exceptions.HTTPError as http_err:
return {
'statusCode': 500,
'body': json.dumps(f'HTTP error occurred: {http_err}')
}
except requests.exceptions.ConnectionError as conn_err:
return {
'statusCode': 500,
'body': json.dumps(f'Connection error occurred: {conn_err}')
}
except requests.exceptions.Timeout as timeout_err:
return {
'statusCode': 500,
'body': json.dumps(f'Timeout error occurred: {timeout_err}')
}
except requests.exceptions.RequestException as req_err:
return {
'statusCode': 500,
'body': json.dumps(f'Request error occurred: {req_err}')
}
prefixes_data = response.json()
ip_ranges = []
# プレフィックスデータからIPv4のIP範囲を抽出
for prefix in prefixes_data['data']['ipv4_prefixes']:
ip_ranges.append(prefix['prefix'])
try:
# WAF IPセットの最新バージョンを取得, NameをWeb Aclのリストに修正
response = waf_client.get_ip_set(
Name='test-kaji-web-acl-Custom-ipaddress-blocklist',
Scope=WAF_SCOPE,
Id=WAF_IP_SET_ID
)
lock_token = response['LockToken']
current_ip_set = response['IPSet']['Addresses']
# IPセットを更新, NameをWeb Aclのリストに修正
response = waf_client.update_ip_set(
Name='test-kaji-web-acl-Custom-ipaddress-blocklist',
Scope=WAF_SCOPE,
Id=WAF_IP_SET_ID,
Addresses=ip_ranges,
LockToken=lock_token
)
except boto3.exceptions.Boto3Error as boto_err:
return {
'statusCode': 500,
'body': json.dumps(f'Error occurred while get or updating WAF IP: {boto_err}')
return {
'statusCode': 200,
'body': json.dumps('IP set updated successfully')
}
LambdaのIAM Role
wafv2:GetIPSetは、既存のIPセットの情報を取得するための権限です。また、wafv2:UpdateIPSetは、IPセットに新しいIPアドレス範囲を追加するために必要な権限です。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:<region>:<account-id>:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:<region>:<account-id>:log-group:/aws/lambda/<lambda-function-name>:*"
]
},
{
"Effect": "Allow",
"Action": [
"wafv2:GetIPSet",
"wafv2:UpdateIPSet",
"wafv2:ListIPSets"
],
"Resource": "arn:aws:wafv2:<region>:<account-id>:global/ipset/*"
}
]
}
Python Requestライブラリの準備(Lambda Layerの作成)
CloudShellでRequestライブラリの準備し、S3へ保存します。
# pyenvの依存パッケージをインストールする:
sudo yum install -y gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel xz xz-devel libffi-devel git
# pyenvをインストールする:
curl https://pyenv.run | bash
# インストール後、以下のコマンドを実行して環境変数を設定します(.bashrcに追加します)。
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# これを実行した後、次のようにして設定を反映します:
source ~/.bashrc
# pyenvでPython 3.12をインストールします。
pyenv install 3.12.0
pyenv global 3.12.0
python --version
# 必要なディレクトリを作成
mkdir -p lambda_layer/python/lib/python3.12/site-packages/
# requestsライブラリをインストール
pip install requests -t lambda_layer/python/lib/python3.12/site-packages/
# Lambda Layer用のファイルをZIPに圧縮
cd lambda_layer
zip -r9 ../layer_requests.zip .
# S3バケットがない場合は作成
aws s3 mb s3://your-bucket-name
# ZIPファイルをS3にアップロード
aws s3 cp ../layer_requests.zip s3://your-bucket-name/
Lambda Layerの適用
- AWS Lambdaコンソールに移動し、「レイヤーの作成」を選択します。
- 先ほどS3にアップロードしたrequestのZIPファイルを指定して、Lambda Layerを作成します。
- Python 3.12に対応するLayerとして作成します。
LambdaのIAM Role
以下のポリシーを「カスタムポリシー」として作成し、Lambda用のIAMロールにアタッチします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:<region>:<account-id>:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:<region>:<account-id>:log-group:/aws/lambda/asn-ip-list-add:*"
]
},
{
"Effect": "Allow",
"Action": [
"wafv2:GetIPSet",
"wafv2:UpdateIPSet",
"wafv2:ListIPSets"
],
"Resource": "arn:aws:wafv2:<region>:<account-id>:global/ipset/*"
}
]
}
試してみた
- 簡単に試せる自分の利用している回線キャリアのBGP ASNを用いてカウントモードでお試しください
- Lambdaをテスト実行するとIP setsにIPアドレスリストが追加され、カウントされることを確認します。
まとめ
今回の実装では、まだエラーハンドリングに改善の余地があるかもしれませんが、基本的なフローは実現できています。より高度な実装としては、Lambda@EdgeやCloudFront Functionsを活用する方法も考えられますが、今回の方法はシンプルでスケーラブルと思ってます。
どなたかのお役に立てれば光栄です。